/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package org.atomojo.sparql;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;

/**
 *
 * @author alex
 */
public class Lexer {

   public enum Type {
      EOF,
      Literal,
      IRI,
      Variable,
      Identifier,
      QName,
      OpenCurlyBracket,
      CloseCurlyBracket,
      OpenParen,
      CloseParen,
      OpenSquareBracket,
      CloseSquareBracket,
      Comma,
      Period,
      SemiColon,
      Colon,
      Asterik,
      OR,
      AND,
      EQUALS,
      NOT_EQUALS
   }
   
   public enum Keyword {
      NONE,
      BASE,
      PREFIX,
      SELECT,
      CONSTRUCT,
      DESCRIBE,
      ASK,
      ORDER,
      BY,
      LIMIT,
      OFFSET,
      DISTINCT,
      REDUCED,
      FROM,
      NAMED,
      WHERE,
      GRAPH,
      OPTIONAL,
      UNION,
      FILTER,
      A,
      STR,
      LANG,
      LANGMATCHES,
      DATATYPE,
      BOUND,
      sameTERM,
      isURI,
      isIRI,
      isLITERAL,
      REGEX,
      TRUE,
      FALSE
   }
   
   public static class Token {
      Type type;
      int line;
      int column;
      String value;
      Keyword keyword;
      public Token(Type type,int line, int column,String value) {
         this.type = type;
         this.line = line;
         this.column = column;
         this.value = value;
         this.keyword = null;
      }
      
      public Type getType() {
         return type;
      }
      public Keyword getKeyword() {
         if (keyword==null && value!=null) {
            if (value.equalsIgnoreCase("BASE")) {
               keyword = Keyword.BASE;
            } else if (value.equalsIgnoreCase("PREFIX")) {
               keyword = Keyword.PREFIX;
            } else if (value.equalsIgnoreCase("SELECT")) {
               keyword = Keyword.SELECT;
            } else if (value.equalsIgnoreCase("CONSTRUCT")) {
               keyword = Keyword.CONSTRUCT;
            } else if (value.equalsIgnoreCase("DESCRIBE")) {
               keyword = Keyword.DESCRIBE;
            } else if (value.equalsIgnoreCase("ASK")) {
               keyword = Keyword.ASK;
            } else if (value.equalsIgnoreCase("ORDER")) {
               keyword = Keyword.ORDER;
            } else if (value.equalsIgnoreCase("BY")) {
               keyword = Keyword.BY;
            } else if (value.equalsIgnoreCase("LIMIT")) {
               keyword = Keyword.LIMIT;
            } else if (value.equalsIgnoreCase("OFFSET")) {
               keyword = Keyword.OFFSET;
            } else if (value.equalsIgnoreCase("DISTINCT")) {
               keyword = Keyword.DISTINCT;
            } else if (value.equalsIgnoreCase("REDUCED")) {
               keyword = Keyword.REDUCED;
            } else if (value.equalsIgnoreCase("FROM")) {
               keyword = Keyword.FROM;
            } else if (value.equalsIgnoreCase("NAMED")) {
               keyword = Keyword.NAMED;
            } else if (value.equalsIgnoreCase("WHERE")) {
               keyword = Keyword.WHERE;
            } else if (value.equalsIgnoreCase("GRAPH")) {
               keyword = Keyword.GRAPH;
            } else if (value.equalsIgnoreCase("OPTIONAL")) {
               keyword = Keyword.OPTIONAL;
            } else if (value.equalsIgnoreCase("UNION")) {
               keyword = Keyword.UNION;
            } else if (value.equalsIgnoreCase("FILTER")) {
               keyword = Keyword.FILTER;
            } else if (value.equals("a")) {
               keyword = Keyword.A;
            } else if (value.equalsIgnoreCase("STR")) {
               keyword = Keyword.STR;
            } else if (value.equalsIgnoreCase("LANG")) {
               keyword = Keyword.LANG;
            } else if (value.equalsIgnoreCase("LANGMATCHES")) {
               keyword = Keyword.LANGMATCHES;
            } else if (value.equalsIgnoreCase("DATATYPE")) {
               keyword = Keyword.DATATYPE;
            } else if (value.equalsIgnoreCase("BOUND")) {
               keyword = Keyword.BOUND;
            } else if (value.equalsIgnoreCase("sameTERM")) {
               keyword = Keyword.sameTERM;
            } else if (value.equalsIgnoreCase("isURI")) {
               keyword = Keyword.isURI;
            } else if (value.equalsIgnoreCase("isIRI")) {
               keyword = Keyword.isIRI;
            } else if (value.equalsIgnoreCase("isLITERAL")) {
               keyword = Keyword.isLITERAL;
            } else if (value.equalsIgnoreCase("REGEX")) {
               keyword = Keyword.REGEX;
            } else if (value.equalsIgnoreCase("TRUE")) {
               keyword = Keyword.TRUE;
            } else if (value.equalsIgnoreCase("FALSE")) {
               keyword = Keyword.FALSE;
            } else {
               keyword = Keyword.NONE;
            }
         }
         return keyword;
      }

      public int getLine() {
         return line;
      }

      public int getColumn() {
         return column;
      }

      public String getValue() {
         return value;
      }
   }
   
   int line;
   int column;
   Reader input;
   Token current;
   public Lexer(Reader r) {
      this.input = new BufferedReader(r,1024);
      this.line = 1;
      this.column = 0;
      this.current = null;
   }
   
   protected void undo() 
      throws IOException
   {
      input.reset();
   }
   protected int nextChar() 
      throws IOException,ParseException
   {
      input.mark(1);
      int ch = input.read();
      if (ch<0) {
         return -1;
      }
      column++;
      if (ch=='\n') {
         column = 1;
         line++;
      }
      if (ch=='\\') {
         // possible we have an escaped character
         input.mark(1);
         int next = input.read();
         if (next=='u') {
            // we have a 4-digit escaped character
            StringBuilder hex = new StringBuilder();
            for (int i=0; ch>0 && i<4; i++) {
               ch = input.read();
               if (ch>0) {
                  hex.append((char)ch);
               }
            }
            try {
               return Integer.parseInt(hex.toString(),16);
            } catch (NumberFormatException ex) {
               throw new ParseException("Bad hexidecimal value "+hex.toString()+" for character.",line,column);
            }
         } else if (next=='U') {
            // we have a 8-digit escaped character
            StringBuilder hex = new StringBuilder();
            for (int i=0; ch>0 && i<8; i++) {
               ch = input.read();
               if (ch>0) {
                  hex.append((char)ch);
               }
            }
            try {
               return Integer.parseInt(hex.toString(),16);
            } catch (NumberFormatException ex) {
               throw new ParseException("Bad hexidecimal value "+hex.toString()+" for character.",line,column);
            }
         } else {
            input.reset();
         }
      }
      return ch;
   }
   
   public Token peek()
      throws IOException,ParseException
   {
      current = next();
      return current;
   }
   
   public Token next()
      throws IOException,ParseException
   {
      if (current!=null) {
         Token retval = current;
         current = null;
         return retval;
      }
      int ch = nextChar();
      if (ch<0) {
         return new Token(Type.EOF,line,column,null);
      }
      while (ch>=0 && Character.isWhitespace(ch) || ch=='#') {
         if (ch=='#') {
            // dump comments
            do {
               ch = nextChar();
            } while (ch>=0 && ch!='\n');
         } else {
            // advance because it is whitespace
            ch = nextChar();
         }
      }
      switch (ch) {
         case '\'':
         case '"':
            return parseLiteral(ch);
         case '{':
            return new Token(Type.OpenCurlyBracket,line,column,"{");
         case '}':
            return new Token(Type.CloseCurlyBracket,line,column,"}");
         case '[':
            return new Token(Type.OpenSquareBracket,line,column,"[");
         case ']':
            return new Token(Type.CloseSquareBracket,line,column,"]");
         case '(':
            return new Token(Type.OpenParen,line,column,"(");
         case ')':
            return new Token(Type.CloseParen,line,column,")");
         case ',':
            return new Token(Type.Comma,line,column,",");
         case '.':
            return new Token(Type.Period,line,column,".");
         case ';':
            return new Token(Type.SemiColon,line,column,";");
         case ':':
            return new Token(Type.Colon,line,column,":");
         case '*':
            return new Token(Type.Asterik,line,column,"*");
         case '=':
            return new Token(Type.EQUALS,line,column,"=");
         case '<':
            return parseIRI();
         case '|':
            if ((ch=nextChar())=='|') {
               return new Token(Type.OR,line,column,"||");
            }
            break;
         case '&':
            if ((ch=nextChar())=='&') {
               return new Token(Type.AND,line,column,"&&");
            }
            break;
         case '!':
            if ((ch=nextChar())=='=') {
               return new Token(Type.NOT_EQUALS,line,column,"!=");
            }
            break;
         case '$':
         case '?':
            return parseIdentifier(-1,Type.Variable);
         default:
            if (Character.isUnicodeIdentifierStart(ch)) {
               return parseIdentifier(ch,Type.Identifier);
            }
      }
      throw new ParseException("Unrecognized character: '"+((char)ch),line,column);
   }
   
   public int getLine() {
      return line;
   }
   
   public int getColumn() {
      return column;
   }
   
   protected Token parseLiteral(int end) 
      throws IOException,ParseException
   {
      int ch = end;
      StringBuilder buffer = new StringBuilder();
      do {
         ch = nextChar();
         if (ch=='\\') {
            ch = nextChar();
            switch (ch) {
               case -1:
                  throw new ParseException("Unexpected end of file in literal.",line,column);
               case 't':
                  buffer.append('\t');
                  break;
               case 'n':
                  buffer.append('\n');
                  break;
               case 'r':
                  buffer.append('\r');
                  break;
               case 'b':
                  buffer.append('\b');
                  break;
               case 'f':
                  buffer.append('\f');
                  break;
               case '\\':
               case '\'':
               case '"':
                  buffer.append(ch);
                  break;
               default:
                  throw new ParseException("Unrecognized escape sequence \\"+((char)ch),line,column);
                  
            }
         } else if (ch>=0 && ch!=end) {
            
            buffer.append((char)ch);
         }
      } while (ch>=0 && ch!=end);
      if (ch!=end) {
         throw new ParseException("Unexpected end of file in literal.",line,column);
      }
      return new Token(Type.Literal,line,column,buffer.toString());
   }
   protected Token parseIRI() 
      throws IOException,ParseException
   {
      int ch = 0;
      StringBuilder buffer = new StringBuilder();
      do {
         ch = nextChar();
         if (ch>=0 && ch!='>') {
            
            buffer.append((char)ch);
         }
      } while (ch>=0 && ch!='>');
      if (ch!='>') {
         throw new ParseException("Unexpected end of file in IRI.",line,column);
      }
      return new Token(Type.IRI,line,column,buffer.toString());
   }
   
   protected Token parseIdentifier(int start,Type type) 
      throws IOException,ParseException
   {
      int ch = start<0 ? nextChar() : start;
      if (ch<0) {
         throw new ParseException("Unexpected end of file in variable name.",line,column);
      }
      boolean ok = Character.isUnicodeIdentifierStart(ch);
      if (!ok) {
         throw new ParseException("Illegal start character "+((char)ch)+" in variable.",line,column);
      }
      StringBuilder buffer = new StringBuilder();
      buffer.append((char)ch);
      boolean qname = false;
      while (ch>=0 && ok) {
         ch = nextChar();
         if (ch<0) {
            throw new ParseException("Unexpected end of file in variable name.",line,column);
         }
         if (!qname && type==Type.Identifier && ch==':') {
            ok = true;
            qname = true;
         } else {
            ok = Character.isUnicodeIdentifierPart(ch);
         }
         if (ok) {
            buffer.append((char)ch);
         } else {
            undo();
         }
      } 
      return new Token(qname ? Type.QName : type,line,column,buffer.toString());
   }
}
